import { queueClass } from './libvbar-m-dxqueue.so'
import map from './dxMap.js'

/**
 * Thread-Safe FIFO Queue Module (dxQueue)
 *
 * Features:
 * - Provides a topic-based, thread-safe, in-memory FIFO (First-In, First-Out) queue.
 * - Guarantees thread safety for all operations across multiple JavaScript threads.
 * - Supports storing various data types (string, number, boolean, object, array) by automatically serializing/deserializing them.
 * - Fixed-size queues: The maximum size of each queue is defined at initialization.
 *
 * Usage:
 * - First, initialize a named queue with a maximum size using `queue.init('myQueue', 1000)`.
 * - Then, get an instance of that queue in any thread using `queue.getInstance('myQueue')`.
 * - Use the instance's methods (`push`, `pop`, `size`, etc.) to interact with the queue.
 *
 * @example
 * import queue from 'dxQueue';
 *
 * // Initialize a queue for user events with a max size of 100
 * const eventQueue = queue.init('user_events', 100);
 *
 * // Push some data
 * eventQueue.push({ event: 'login', userId: 123 });
 * eventQueue.push({ event: 'click', element: 'button#submit' });
 *
 * // Get queue size
 * const currentSize = eventQueue.size(); // 2
 *
 * // Pop data
 * const firstEvent = eventQueue.pop(); // { event: 'login', userId: 123 }
 */
const queueObj = new queueClass();
const createdQueues = map.get("dx_queues");

/**
 * @typedef {object} DxQueueInstance
 * @property {function(): *} pop - Removes and returns the item at the beginning of the queue. Returns `null` if the queue is empty.
 * @property {function(*): boolean} push - Adds an item to the end of the queue. Returns `true` on success. Throws an error if the queue is full.
 * @property {function(): number} size - Returns the current number of items in the queue.
 * @property {function(): number} getMaxSize - Returns the maximum size of the queue.
 * @property {function(): boolean} isFull - Checks if the queue is full.
 * @property {function(): boolean} destroy - Destroys this specific queue, freeing its resources. Returns `true` on success.
 */

function base(name, maxSize) {

    /**
     * Removes and returns the item at the beginning of the queue.
     * @returns {*} The item at the beginning of the queue, deserialized to its original type. Returns `null` if the queue is empty.
     */
    function pop() {
        if (!name || name.length < 1) {
            throw new Error("dxQueue.pop: name should not be null or empty")
        }
        const value = queueObj.pop(name)
        // Deserialization return value
        return value === null ? null : _parseString(value)
    }

    /**
     * Adds an item to the end of the queue.
     * @param {*} value The value to add to the queue. Supported types: string, number, boolean, object, array. Functions are not supported.
     * @returns {boolean} Returns `true` on success.
     * @throws {Error} Throws an error if the queue is full, or if the value is of an unsupported type (e.g., a function).
     */
    function push(value) {
        if (!name || name.length < 1) {
            throw new Error("dxQueue.push: name should not be null or empty")
        }
        if (value === null || value === undefined) {
            throw new Error("dxQueue.push: value should not be null or undefined")
        }

        // Check queue length limit
        if (maxSize !== undefined && maxSize > 0) {
            const currentSize = queueObj.size(name)
            if (currentSize >= maxSize) {
                throw new Error(`dxQueue.push: queue '${name}' is full (${currentSize}/${maxSize})`)
            }
        }

        // Serialize the value and push it into the queue
        const serializedValue = _stringifyValue(value)
        return queueObj.push(name, serializedValue)
    }

    /**
     * Returns the current number of items in the queue.
     * @returns {number} The number of items in the queue.
     */
    function size() {
        if (!name || name.length < 1) {
            throw new Error("dxQueue.size: name should not be null or empty")
        }
        return queueObj.size(name)
    }

    /**
     * Returns the maximum size of the queue.
     * @returns {number} The maximum number of items the queue can hold.
     */
    function getMaxSize() {
        return maxSize
    }

    /**
     * Checks if the queue has reached its maximum size.
     * @returns {boolean} `true` if the queue is full, `false` otherwise.
     */
    function isFull() {
        if (maxSize === undefined || maxSize <= 0) {
            return false // An unlimited queue will never be full.
        }
        return queueObj.size(name) >= maxSize
    }

    /**
     * Destroys this specific queue, deleting all its items and freeing associated memory.
     * After calling destroy, this instance should not be used anymore.
     * @returns {boolean} Returns `true` on success, `false` if the queue was already destroyed or not found.
     */
    function destroy() {
        if (!name || name.length < 1) {
            throw new Error("dxQueue.destroy: name should not be null or empty")
        }
        const result = queueObj.destroy(name)
        if (result) {
            createdQueues.del(name);
        }
        return result
    }

    return { pop, push, size, getMaxSize, isFull, destroy };
}

/**
 * Manages all dxQueue instances.
 */
const queue = {
    /**
     * Initializes a new queue with a specific name and maximum size.
     * Throws an error if a queue with the same name already exists.
     * @param {string} name The unique name for the queue. Must not be null or empty.
     * @param {number} maxSize The maximum number of items the queue can hold. Must be a non-negative integer. Use 0 for an unlimited size.
     * @returns {DxQueueInstance} An object with methods to interact with the newly created queue.
     * @throws {Error} If the name is invalid, maxSize is invalid, or a queue with the same name already exists.
     */
    init: function (name, maxSize) {
        if (!name || name.length === 0) {
            throw new Error("dxQueue.init: name should not be null or empty");
        }
        if (typeof maxSize !== 'number' || maxSize < 0 || !Number.isInteger(maxSize)) {
            throw new Error("dxQueue.init: maxSize must be a non-negative integer");
        }
        if (createdQueues.has(name)) {
            throw new Error(`dxQueue.init: queue '${name}' already exists`);
        }

        // Attempt to create the queue (it will not be created again if it already exists).
        queueObj.create(name);

        // Only `maxSize` is stored in the shared Map because it can be safely passed between threads.
        createdQueues.put(name, maxSize);

        // Returns the local queue operation object
        return base(name, maxSize);
    },

    /**
     * Gets an instance of a previously initialized queue.
     * @param {string} name The name of the queue to retrieve. Must not be null or empty.
     * @returns {DxQueueInstance} An object with methods to interact with the specified queue.
     * @throws {Error} If the name is invalid, or if the queue has not been initialized with `init()`.
     */
    getInstance: function (name) {
        if (!name || name.length === 0) {
            throw new Error("dxQueue.getInstance: name should not be null or empty");
        }
        if (!createdQueues.has(name)) {
            throw new Error(`dxQueue.getInstance: queue '${name}' has not been initialized. Call init() first.`);
        }
        // Retrieve maxSize from the shared Map and create a new operation object in the current thread/context.
        const maxSize = createdQueues.get(name);
        return base(name, maxSize);
    },

    /**
     * @deprecated Use init() and getInstance() instead.
     */
    get: function (name) {
        throw new Error("dxQueue.get is deprecated. Use dxQueue.init() to create a queue and dxQueue.getInstance() to get an existing queue.");
    },

    /**
     * Clears all queues, removing all elements from every queue and releasing
     * their associated memory. The queue module itself remains initialized and 
     * new queues can be created after calling this.
     *
     * IMPORTANT: This is a global operation that affects all queues.
     * To destroy a single queue, use the `.destroy()` method on an instance.
     */
    clearAll: function () {
        queueObj.clearAll();
        // The underlying C map is cleared, so we should also clear the JS map.
        // dxMap's destroy clears the current topic map.
        createdQueues.destroy();
    },
}

/**
 * Serializes a value into a string with a type prefix.
 * @private
 * @param {*} value - The value to serialize.
 * @returns {string} The serialized string.
 * @throws {Error} If the value is a function or unsupported type.
 */
function _stringifyValue(value) {
    const type = typeof value
    if (type === 'string') {
        return value
    }
    if (type === 'number') {
        return '#n#' + value
    }
    if (type === 'boolean') {
        return '#b#' + value
    }
    if (type === 'object') {
        // If it is an object, further check if it is an array.
        if (Array.isArray(value)) {
            return '#a#' + JSON.stringify(value);
        }
        return '#o#' + JSON.stringify(value)
    }
    if (type === 'function') {
        throw new Error("dxQueue.push: value should not be a function")
    }
    // Other types (such as symbol, bigint, etc.) are not supported.
    throw new Error(`dxQueue.push: unsupported value type: ${type}`)
}

/**
 * Deserializes a string with a type prefix back to its original value.
 * @private
 * @param {string} str - The string to deserialize.
 * @returns {*} The deserialized value.
 */
function _parseString(str) {
    if (str.startsWith('#n#')) {
        // Analyzing numbers
        const numberStr = str.substring(3);
        return numberStr.includes('.') ? parseFloat(numberStr) : parseInt(numberStr, 10);
    } else if (str.startsWith('#b#')) {
        // Parsing Boolean values
        return str.substring(3) === 'true';
    } else if (str.startsWith('#a#')) {
        // parse array
        return JSON.parse(str.substring(3));
    } else if (str.startsWith('#o#')) {
        // parsing objects
        return JSON.parse(str.substring(3));
    } else {
        // By default, the string is returned.
        return str;
    }
}

export default queue;

